/*
 * Copyright (C) 2010 Fores Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package jp.fores.midori.client.handler;

import java.io.BufferedInputStream;
import java.io.InputStream;
import java.net.Socket;
import java.net.URL;
import java.util.zip.GZIPInputStream;

import jp.fores.midori.client.RPCClient;
import jp.fores.midori.core.dto.RPCInputDto;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.HttpVersion;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.EntityTemplate;
import org.apache.http.impl.DefaultHttpClientConnection;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.BasicHttpProcessor;
import org.apache.http.protocol.ExecutionContext;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpRequestExecutor;
import org.apache.http.protocol.RequestConnControl;
import org.apache.http.protocol.RequestContent;
import org.apache.http.protocol.RequestTargetHost;

/**
 * リモートプロシージャーコール用を行うクライアントの通信処理を行うハンドラのHttpCoreを使った実装クラス。<br>
 * <a href="http://hc.apache.org/index.html">Apache HttpComponents</a>の <code>{@link HttpRequestExecutor}</code> クラスを使って処理を行います。<br>
 * (このクラスの全てのメソッドはスレッドセーフです。)<br>
 */
public class HttpCoreRPCHandler implements RPCHandler {
	//==========================================================
	//定数

	/**
	 * ログ出力用
	 */
	private static final Log log = LogFactory.getLog(HttpCoreRPCHandler.class);

	/**
	 * バッファサイズ
	 */
	private static final int BUFFER_SIZE = 8192;


	//==========================================================
	//フィールド

	//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	//クラスフィールド

	/**
	 * HttpProcessor
	 */
	//(スレッドセーフなクラスなので、クラスフィールドとして定義して共有する)
	private static final BasicHttpProcessor httpProcessor = new BasicHttpProcessor();

	/**
	 * HttpRequestExecutor
	 */
	//(スレッドセーフなクラスなので、クラスフィールドとして定義して共有する)
	private static final HttpRequestExecutor httpRequestExecutor = new HttpRequestExecutor();


	//==========================================================
	//メソッド

	//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	//staticイニシャライザ
	static {
		//HttpProcessorに各種Interceptorを設定
		httpProcessor.addInterceptor(new RequestContent());
		httpProcessor.addInterceptor(new RequestTargetHost());
		httpProcessor.addInterceptor(new RequestConnControl());

		//「User-Agent」ヘッダは自分で設定するのでコメントアウト
		//httpProcessor.addInterceptor(new RequestUserAgent());

		//「Expect: 100-Continue handshake」機能は使わないのでコメントアウト
		//httpProcessor.addInterceptor(new RequestExpectContinue());
	}


	//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	//抽象メソッドの実装

	/**
	 * {@inheritDoc}
	 */
	@Override
	public Object execute(RPCClient rpcClient, URL url, RPCInputDto inputDto)
			throws Exception {
		//==========================================================
		//リクエストの設定

		//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
		//URLをHttpHostに変換

		//URLのポートを取得
		int port = url.getPort();

		//ポートを取得できなかった場合
		if (port == -1) {
			//代わりに「80」を使う
			port = 80;
		}

		//URLのホストとポートを元にして、HttpHostのインスタンスを生成
		HttpHost host = new HttpHost(url.getHost(), port);


		//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
		//リクエストメソッドに応じたクラスを使用する

		//リクエストメソッドを大文字にして取得
		String requestMethod = rpcClient.requestMethod.toUpperCase();

		//リクエスト用のクラス
		//(setEntity()ができないとサーバー側にデータを送れないので、型はHttpEntityEnclosingRequestBaseに限定する)
		final HttpEntityEnclosingRequestBase request;

		//リクエストメソッドが「POST」の場合
		if (HttpPost.METHOD_NAME.equals(requestMethod)) {
			//POST用のリクエストクラスのインスタンスを使用する
			request = new HttpPost(url.toURI());
		}
		//リクエストメソッドが「PUT」の場合
		else if (HttpPut.METHOD_NAME.equals(requestMethod)) {
			//PUT用のリクエストクラスのインスタンスを使用する
			request = new HttpPut(url.toURI());
		}
		//それ以外の場合
		else {
			//どうしようもないので、不正な引数の場合の例外を投げる
			//(メッセージには大文字に変換する前のリクエストメソッドを含める)
			throw new IllegalArgumentException("リクエストメソッドが不正です。:"
					+ rpcClient.requestMethod);
		}


		//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
		//パラメーターの設定
		//(TODO:設定方法が本当にあっているかもう少し確認する)

		//パラメータークラスのインスタンスを生成
		HttpParams params = new BasicHttpParams();

		//HTTPのバージョンに「1.1」を設定
		HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);

		//文字コードに「UTF-8」を設定
		HttpProtocolParams.setContentCharset(params, "UTF-8");

		//「Expect: 100-Continue handshake」機能は使わないので無効にする
		HttpProtocolParams.setUseExpectContinue(params, false);

		//接続タイムアウト時間を設定
		params.setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, rpcClient.connectTimeout);

		//読み取りタイムアウト時間を設定
		params.setIntParameter(CoreConnectionPNames.SO_TIMEOUT, rpcClient.readTimeout);


		//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
		//リクエストヘッダの設定

		//「User-Agent」ヘッダに自分の単純なクラス名を設定
		request.addHeader("User-Agent", this.getClass().getSimpleName());

		//「Content-Type」ヘッダを設定
		//(rpcClient本体ではなく、rpcClientの中のserializerから取得する)
		request.addHeader("Content-Type", rpcClient.serializer.getContentType());

		//リクエスト送信時にgzip圧縮を行う場合
		if (rpcClient.requestGZip) {
			//「Content-Encoding」用のヘッダを設定
			request.addHeader(rpcClient.getContentEncodingHeaderName(), "gzip");
		}

		//レスポンスでgzip圧縮を受け入れる場合
		if (rpcClient.responseGZip) {
			//「Accept-Encoding」用のヘッダを設定
			request.addHeader(rpcClient.getAcceptEncodingHeaderName(), "gzip,deflate");
		}


		//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
		//コンテキストの設定

		//コンテキストクラスのインスタンスを生成
		//(親コンテキストは存在しないのでnullを指定する)
		HttpContext context = new BasicHttpContext(null);

		//コネクションとしてDefaultHttpClientConnectionクラスのインスタンスを生成
		DefaultHttpClientConnection connection = new DefaultHttpClientConnection();

		//コンテキストにコネクションを設定
		context.setAttribute(ExecutionContext.HTTP_CONNECTION, connection);

		//コンテキストにHttpHostを設定
		context.setAttribute(ExecutionContext.HTTP_TARGET_HOST, host);


		//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
		//サーバーに入力データを渡す

		//入力データ用のDTOの内容を出力ストリーム経由でEntityに出力するためのクラスを使う
		//(標準で用意されているEntityでは直接出力ストリームには出力できないため)
		request.setEntity(new EntityTemplate(new InternalContentProducer(rpcClient, inputDto)));


		try {
			//==========================================================
			//コネクションを開く

			//HttpHostのホスト名とポートを元にして、ソケットクラスのインスタンスを生成
			Socket socket = new Socket(host.getHostName(), host.getPort());

			//ソケットとパラメーターを元にして、コネクションを開く
			connection.bind(socket, params);


			//==========================================================
			//サーバー呼び出しを行い、レスポンスを取得する

			//リクエストにパラメーターを設定する
			request.setParams(params);

			//リクエスト送信の前処理を行う
			httpRequestExecutor.preProcess(request, httpProcessor, context);

			//実際に通信を行い、レスポンスを取得する
			HttpResponse response = httpRequestExecutor.execute(request, connection, context);

			//レスポンスにパラメーターを設定する
			response.setParams(params);

			//リクエスト送信の後処理を行う
			httpRequestExecutor.postProcess(response, httpProcessor, context);

			//レスポンスのEntityを取得
			HttpEntity responseEntity = response.getEntity();

			//レスポンスのEntityの入力ストリームを取得
			InputStream inputStream = responseEntity.getContent();

			try {
				//レスポンスの「Content-Encoding」用のヘッダの内容を取得
				Header contentEncoding = response.getFirstHeader(rpcClient.getContentEncodingHeaderName());

				//レスポンスの「Content-Encoding」用のヘッダに「gzip」という文字列が含まれている場合
				if ((contentEncoding != null)
						&& (contentEncoding.getValue().indexOf("gzip") != -1)) {
					//デバッグログが有効な場合
					if (log.isDebugEnabled()) {
						//デバッグログ出力
						log.debug("レスポンスがgzip圧縮されているので、展開しつつ読み込みます。");
					}

					//入力ストリームをGZIPInputStreamでラッピングする
					inputStream = new GZIPInputStream(inputStream);
				}
				//それ以外の場合
				else {
					//入力ストリームをBufferedInputStreamでラッピングする
					inputStream = new BufferedInputStream(inputStream, BUFFER_SIZE);
				}

				//レスポンスの入力ストリームを処理して、リモートプロシージャコールの戻り値を取得
				//(rpcClientの中のserializerに処理を委譲する)
				Object result = rpcClient.serializer.handleRPCResponseInputStream(inputStream);

				//デバッグログが有効な場合
				if (log.isDebugEnabled()) {
					//デバッグログ出力
					log.debug("*** 「" + url + "」に対するリモートプロシージャコールが正常終了しました ***");
				}

				//リモートプロシージャコールの戻り値を返す
				return result;
			}
			//終了処理
			finally {
				//入力ストリームを確実に閉じる
				if (inputStream != null) {
					try {
						inputStream.close();
					}
					//例外処理
					catch (Exception e) {
						//どうしようもないので、例外はもみ消す
					}
				}
			}
		}
		//終了処理
		finally {
			//コネクションを閉じる
			connection.close();
		}
	}
}
